/* ***** BEGIN LICENSE BLOCK *****
 *   Version: MPL 1.1/GPL 2.0/LGPL 2.1
 *
 * The contents of this file are subject to the Mozilla Public License Version
 * 1.1 (the "License"); you may not use this file except in compliance with
 * the License. You may obtain a copy of the License at
 * http://www.mozilla.org/MPL/
 * 
 * Software distributed under the License is distributed on an "AS IS" basis,
 * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
 * for the specific language governing rights and limitations under the
 * License.
 *
 * The Original Code is JsWs.
 *
 * The Initial Developer of the Original Code is
 * Chris Winberry.
 * Portions created by the Initial Developer are Copyright (C) 2007
 * the Initial Developer. All Rights Reserved.
 *
 * Contributor(s):
 *
 * Alternatively, the contents of this file may be used under the terms of
 * either the GNU General Public License Version 2 or later (the "GPL"), or
 * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
 * in which case the provisions of the GPL or the LGPL are applicable instead
 * of those above. If you wish to allow use of your version of this file only
 * under the terms of either the GPL or the LGPL, and not to allow others to
 * use your version of this file under the terms of the MPL, indicate your
 * decision by deleting the provisions above and replace them with the notice
 * and other provisions required by the GPL or the LGPL. If you do not delete
 * the provisions above, a recipient may use your version of this file under
 * the terms of any one of the MPL, the GPL or the LGPL.
 * 
 * ***** END LICENSE BLOCK ***** */
// netscape.security.PrivilegeManager.enablePrivilege("UniversalPreferencesRead UniversalPreferencesWrite UniversalBrowserRead UniversalBrowserWrite UniversalXPConnect")

/*** FILES & LOCATIONS ***/
const JSWS_ROOT				= "/jsws";					// Root folder for all JsWs files
const JSWS_DOCROOT			= JSWS_ROOT + "/htdocs";	// Folder that contains all served content
const JSWS_SCRIPT_FILEEXT	= "jss";					// File extension for scripts that JsWs will execute
const JSWS_DB_ROOT			= JSWS_ROOT + "/data";		// Folder where SQLite/MozStorage DB files are kept
const JSWS_DB_FILEEXT		= "storage";				// File extension for SQLite/MozStorage DB files

/*** PREFERENCES ***/
const JSWS_PREF_ROOT			= "extensions.jsws.";			// Root of all preference settings
const JSWS_PREF_AUTOSTART		= JSWS_PREF_ROOT + "autostart";	// Flag - should JsWs start automatically
const JSWS_PREF_PORT			= JSWS_PREF_ROOT + "port";		// TCP port JsWs will listen on
const JSWS_PREF_LOCALONLY		= JSWS_PREF_ROOT + "localOnly";	// Flag - should JsWs only accept loopback connections
const JSWS_PREF_THREADED		= JSWS_PREF_ROOT + "threaded";	// Flag - should JsWs run in a separate thread

/*** MODULES ***/
const JSWS_PREF_MODULES_ROOT		= JSWS_PREF_ROOT + "modules.";	// Root of all module registrations
const JSWS_MODULES_ROOTKEY			= "root";						// Reserved module name for "/"
const JSWS_MODULES_IDKEY			= "id";							// Key for modules' extension ID
const JSWS_MODULES_NAMEKEY			= "name";						// Key for modules' display name
const JSWS_MODULES_EXTENSIONPATHKEY	= "extPath";					// Key for modules' root folder
const JSWS_MODULES_CONTENTPATHKEY	= "contentPath";				// Key for modules' location of servable content
const JSWS_MODULES_VIRTUALPATHKEY	= "virtualPath";				// Key for modules' mapping to a JsWs path
const JSWS_MODULES_STARTPAGEKEY		= "startPage";					// Key for modules' default start page
const JSWS_MODULES_HANDLERKEY		= "handler";					// Key for modules' page handler
const JSWS_MODULES_INITPAGEKEY		= "initPage";					// Key for modules' initialization page
const JSWS_MODULES_DESCRIPTIONKEY	= "description";				// Key for modules' description

if (!Cc)
	var Cc = Components.classes;
if (!Ci)
	var Ci = Components.interfaces;

/********************************************************************************
 * Misc Functions
 *******************************************************************************/
/**
 * Makes one "class" inherit from another while maintaining constructor chain
 * and not breaking instanceof usage
 * @param {Class} base constructor to inherited from
 * @param {Class} sub constructor to subclass from base
 */
function SubClass (base, sub) {
	function proxyObject () { }
	proxyObject.prototype = base.prototype;

	var proxy = new proxyObject();
	proxy.constructor = sub;
	sub.baseConstructor = base;
	sub.prototype = proxy;
}


/********************************************************************************
 * Exceptions
 *******************************************************************************/

/**
 * Base class for exceptions to inherit from
 * @class This class forms the basis for all other exceptions. It provides
 *  consistent display formatting and wrapping of earlier/inner exceptions.
 *  Inner exceptions are a convenient way to establish a lazy call stack for
 *  debugging and error reporting.
 * @constructor
 * @param {String} sMsg Description of exception
 * @param {Number} code (optional) Error code
 * @param {Exception} ex (optional) An earlier exception to wrap
 */
function ExBase (sMsg, code, ex) {
	/**
	 * Description of the exception
	 * @type String
	 */
	this.message = "";
	/**
	 * Exception error code
	 * @type Number
	 */
	this.code = 0;
	/**
	 * Earlier exception wrapped by the current exception
	 * @type Exception
	 */
	this.originalEx = null;

	this.init(sMsg, code, ex);
}


/** @private */
ExBase.prototype.init = function (sMsg, code, ex) {
	if (code != null)
		this.code = code;
	this.message = sMsg;
	if (ex != null)
		this.originalEx = ex;
}


/**
 * @returns Name of the exception type
 * @type String
 */
ExBase.prototype.name = function () {
	return /(\w+)\(/.exec(this.constructor.toString())[1];
}


/**
 * Serializer
 * @type String
 */
ExBase.prototype.toString = function () {
	return("{{" + this.name() + "}}"
		+ " [ErrNum: " + this.code + "] :"
		+ " " + this.message
		+ ((this.originalEx != null) ? "\n" + 
			((this.originalEx instanceof ExBase) ? this.originalEx : this.formatExternalEx(this.originalEx))
			:
			"")
		);
}


/** @private */
ExBase.prototype.formatExternalEx = function (ex) {
	return(
		"{{" + ex.name + "}}\n"
		+ "   Msg: " + ex.message + "\n"
		+ "   File: " + ex.fileName + "(Line:" + ex.lineNumber + ")\n"
		+ "   Stack: " + ex.stack
		);
}


/**
 * @class Exception class for request parsing
 * @constructor
 * @base ExBase
 * @param {String} sMsg Description of exception
 * @param {Number} code (optional) Error code
 * @param {Exception} ex (optional) An earlier exception to wrap
 * @see Request
 */
function ExHttpParseError (sMsg, code, ex) { ExHttpParseError.baseConstructor.call(this, sMsg, code, ex); }
SubClass(ExBase, ExHttpParseError);

/**
 * @class Exception used to signal the end of processing for a request
 * @constructor
 * @base ExBase
 * @param {String} sMsg Description of exception
 * @param {Number} code (optional) Error code
 * @param {Exception} ex (optional) An earlier exception to wrap
 * @see Response#end
 */
function ExResponseEnd (sMsg, code, ex) { ExResponseEnd.baseConstructor.call(this, sMsg, code, ex); }
SubClass(ExBase, ExResponseEnd);

/**
 * @class Exception class for server side script parsing
 * @constructor
 * @base ExBase
 * @param {String} sMsg Description of exception
 * @param {Number} code (optional) Error code
 * @param {Exception} ex (optional) An earlier exception to wrap
 * @see EvalCodeHost
 */
function ExScriptParseError (sMsg, code, ex) { ExScriptParseError.baseConstructor.call(this, sMsg, code, ex); }
SubClass(ExBase, ExScriptParseError);

/**
 * @class Exception class for server side script loading
 * @constructor
 * @base ExBase
 * @param {String} sMsg Description of exception
 * @param {Number} code (optional) Error code
 * @param {Exception} ex (optional) An earlier exception to wrap
 * @see EvalCodeHost
 */
function ExScriptLoadError (sMsg, code, ex) { ExScriptLoadError.baseConstructor.call(this, sMsg, code, ex); }
SubClass(ExBase, ExScriptLoadError);

/**
 * @class Exception class for file reading
 * @constructor
 * @base ExBase
 * @param {String} sMsg Description of exception
 * @param {Number} code (optional) Error code
 * @param {Exception} ex (optional) An earlier exception to wrap
 */
//TODO: generalize this into file IO operation exceptions
function ExFileLoadError (sMsg, code, ex) { ExFileLoadError.baseConstructor.call(this, sMsg, code, ex); }
SubClass(ExBase, ExFileLoadError);

/**
 * @class Exception class for DB errors
 * @constructor
 * @base ExBase
 * @param {String} sMsg Description of exception
 * @param {Number} code (optional) Error code
 * @param {Exception} ex (optional) An earlier exception to wrap
 * @see DbConnection
 * @see DbQuery
 */
function ExDbError (sMsg, code, ex) { ExDbError.baseConstructor.call(this, sMsg, code, ex); }
SubClass(ExBase, ExDbError);

/********************************************************************************
 * DBConnection
 *******************************************************************************/

/**
 * @class Represents a column in a DB table
 * @constructor
 * @param {Hash/Record} row Hash of a row from a record set
 * @see DbTable
 */
function DbColumn (row) {
	/**
	 * Name of the column
	 * @type String
	 */
	this.name = row["name"];
	/**
	 * Columns data type
	 * @type String
	 */
	this.type = row["type"].toLowerCase();
	/**
	 * ID of the column
	 * @type String
	 */
	this.id = row["cid"];
	/**
	 * Column's default value
	 * @type String
	 */
	this.defVal = row["dflt_value"];
	/**
	 * Flag indicating whether the column is a primary key
	 * @type Boolean
	 */
	this.pk = (row["pk"] == 1);
	/**
	 * Flag indicating if the column allows null values
	 * @type Boolean
	 */
	this.allowNull = (row["notnull"] == 1);
}


/**
 * @class Looks up and represents metadata for a DB table
 * @constructor
 * @param {String} name Name of the table
 * @param {DbConnection} db Database connection to schema that contains the table
 * @requires DbColumn
 */
function DbTable (name, db) {
	/**
	 * Name of the table
	 * @type String
	 */
	this.name = name;
	/**
	 * Columns of the table
	 * @type DbColumn[]
	 */
	this.cols = new Array();

	var query = db.prepare("PRAGMA TABLE_INFO('" + this.name + "')");
	var results = query.exec();
	for (var i = 0; i < results.length; i++)
		this.cols[this.cols.length] = new DbColumn(results[i]);

	query.destroy();
}


/**
 * @class Represents a prepared statement ready for execution with or
 *  without parameters. Execution of the query is handled by {@link #exec}.
 * @constructor
 * @param {DbConnection} dbConn Database connection to schema that contains the table
 * @param {String} sql SQL statement that was prepared
 * @see DbConnection#prepare
 */
function DbQuery (dbConn, sql) {
	/** @private */
	this._conn = dbConn;
	/** @private */
	this._sql = sql;
	/** @private */
	this._query = this._conn.createStatement(this._sql);
}


/**
 * Executes the prepared statement with any parameters passed in
 * @param {Mixed} n Variable number of parameters to be used with the statement
 * @return Data returned by the query
 * @type {Hash/Record}[]
 */
DbQuery.prototype.exec = function (/* n arguments */) {
	for (var i = 0; i < arguments.length; i++) {
		if (arguments[i] == null)
			this._query.bindNullParameter(i);
		else if (typeof(arguments[i]) == "string")
			this._query.bindUTF8StringParameter(i, arguments[i]);
		else if (typeof(arguments[i]) == "number")
			if (parseInt(arguments[i]) != parseFloat(arguments[i]))
				this._query.bindDoubleParameter(i, arguments[i]);
			else
				this._query.bindInt32Parameter(i, arguments[i]);
		else
			this._query.bindUTF8StringParameter(i, arguments[i].toString()); // bindBlobParameter ??? 
	}
	
	var result = new Array();
	while (this._query.executeStep()) {
		var row = new Array();
		for(var i = 0; i < this._query.columnCount; i++)
			row[this._query.getColumnName(i)] = this._query.getUTF8String(i);
		result.push(row);
	}

	this._query.reset();

	return(result);
}


/**
 * Runs cleanup on internal objects
 */
DbQuery.prototype.destroy = function () {
	this._conn = null;
	this._sql = null;
	this._query.reset();
	this._query = null;
}


/**
 * @class Responsible for creating and opening SQLite/MozStorage files. DB files
 *  that don't exist are automatically created. This also provides
 *  methods to enumerate DB files, schema tables, and table columns.
 * @constructor
 * @param {String} name Name of the DB schema to open/create
 */
function DbConnection (name) {
	/** @private */
	this.name = name;
	/** @private */
	this.connection = null;
	this.open();
}


/**
 * Opens the DB. If the DB does not exist, it is first created
 * @exception ExDbError
 */
DbConnection.prototype.open = function () {
	var storageService;
	try {
		var file = this.dbFolder(); // ???
		file.append(this.name + "." + this._extension); // ???

		storageService = Cc["@mozilla.org/storage/service;1"]
			.getService(Ci.mozIStorageService);

		this.connection = storageService.openDatabase(file);
	} catch (ex) {
		this.connection = null;
		throw(new ExDbError("Failed to create/open the database", 0, ex));
	}
}


/**
 * Closes the DB connection
 */
DbConnection.prototype.close = function () {
	if (this.connection == null)
		return;
	this.destroy();
}


/**
 * Builds a list of tables that exist in the schema
 * @return List of tables in the schema
 * @type {@link DbTable}[]
 */
DbConnection.prototype.tables = function () {
	var tables = new Array();
	var query = this.prepare("SELECT * FROM sqlite_master WHERE type = 'table'");
	var results = query.exec();
	for (var i = 0; i < results.length; i++) {
		var row = results[i];
		tables[tables.length] = new DbTable(row["tbl_name"], this);
	}
	query.destroy();
	return(tables);
}


/**
 * Immediately execute a query that returns no data
 * and has no parameters
 * @param {String} sql SQL query to execute
 * @returns Success or failure of the statement execution
 * @type Boolean
 * @exception ExDbError
 */
DbConnection.prototype.execNoResult = function (sql) {
	if (sql == null || sql == '')
		throw(new ExDbError("SQL statement is null or empty"));
	try {
		this.connection.executeSimpleSQL(sql);
	} catch (ex) {
		return(false);
	}
	return(true);
}


/**
 * Prepares a SQL statement that returns data and/or has parameters
 * @param {String} sql SQL query to execute
 * @return Prepare DB query ready for execution
 * @type DbQuery
 * @exception ExDbError
 */
DbConnection.prototype.prepare = function (sql) {
	if (sql == null || sql == '')
		throw(new ExDbError("SQL statement is null or empty"));
	return(new DbQuery(this.connection, sql));
}


/**
 * Runs cleanup on internal objects
 */
DbConnection.prototype.destroy = function () {
	this.connection = null;
}


/** @private */
DbConnection._extension = JSWS_DB_FILEEXT;
/** @ignore */
DbConnection.prototype._extension = DbConnection._extension;

/**
 * Directory where DB files are stored
 * @return Directory where DB files are stored
 * @type nsIFile
 */
DbConnection.dbFolder = function () {
	var file = Cc["@mozilla.org/file/directory_service;1"]
		.getService(Ci.nsIProperties)
		.get("ProfD", Ci.nsIFile);
	JsWsListener.prototype.tools.appendPath(file, JSWS_DB_ROOT);
	return(file);
}


/** @ignore */
DbConnection.prototype.dbFolder = DbConnection.dbFolder;

/**
 * Builds a list of existing DB schemas (files)
 * @return An array of existing DB schemas
 * @type String[]
 */
DbConnection.schemas = function () {
	var file = DbConnection.dbFolder();
	var dirEntries = file.directoryEntries;
	var reStorage = new RegExp("\\/(\\w+)\\." + DbConnection._extension, "i");
	var dbs = new Array();
	while (dirEntries.hasMoreElements()) {
		var dirEntry = dirEntries.getNext().QueryInterface(Ci.nsIFile);
		if (!dirEntry.isDirectory()) {
			var found = reStorage.exec(dirEntry.path)
			if (found)
				dbs.push(found[1]);
		}
	}
	return(dbs);
}


/** @ignore */
DbConnection.prototype.schemas = DbConnection.schemas;

/********************************************************************************
 * EvalCodeHost
 *******************************************************************************/
// TODO: move IO functions to something static

/**
 * @class Container for code executed in the context of a dynamic webpage.
 *  Keeps script page variable/object scope locked to individual requests.
 * @constructor
 * @param {JsWsListener} server JsWs server instance
 * @param {Request} request Incoming request object
 * @param {Response} response Outgoing response object
 * @param {ScriptContext} script Scripting context object
 */
function EvalCodeHost (server, request, response, script) {
	/** @type JsWsListener */
	this.server = server;
	/** @type Request */
	this.request = request;
	/** @type Response */
	this.response = response;
	/** @type ScriptContext */
	this.script = script;
}


/**
 * Runs cleanup on internal objects
 */
EvalCodeHost.prototype.destroy = function () {
	this.server = null;
	this.request = null;
	this.response = null;
	this.script = null;
}


/**
 * Parses a string that contains a mix of static text and/or
 * embedded script into pure JavaScript ready to be eval'd
 * @param {String} sCode String to be parsed
 * @returns JavaScript code ready to be evaluated
 * @type String
 * @exception ExScriptParseError
 */
EvalCodeHost.prototype.parse = function (sCode) {
	var sCodeParsed = "";

	var codeParts = sCode.split(/<\?js\s*/);

	for (var i = 0; i < codeParts.length; i++) {
		if (!codeParts[i].length) { continue; }

		isCodeEnd = (i == 0 || i == codeParts.length);

		codeSectionParts = codeParts[i].split(/\s*\?>/);
		if (isCodeEnd && codeSectionParts.length == 1) {
			codeSectionParts.unshift("");
		}
		if (codeSectionParts.length != 2) { throw(new ExScriptParseError('Unmatched code block markers near: "' + ((codeParts[i].length > 128) ? (codeParts[i].substring(0, 128) + " ...") : codeParts[i]) + '"')); }

		if (codeSectionParts[0].length) {
			if (codeSectionParts[0].match(/^\s*\=/)) {
				sCodeParsed += "response.write(" + codeSectionParts[0].replace(/^\s*=\s*/, '').replace(/;+\s*$/, '') + ");\n";
			} else {
				sCodeParsed += codeSectionParts[0] + "\n";
			}
		}

		if (codeSectionParts[1].length) {
			sCodeParsed += "response.write(\"" + codeSectionParts[1].replace(/\\/g, "\\\\").replace(/'/g, "\\'").replace(/"/g, '\\"').replace(/\r/g, '\\r').replace(/\n/g, '\\n').replace(/\t/g, '\\t') + "\");\n";
		}
	}

	return(sCodeParsed);
}


/** Not implemented yet */
EvalCodeHost.prototype.streamTransfer = function (inStream, outStream, size) {
	// TODO: Support output stream parameter for streaming of URL
}


/**
 * Reads a file from disk
 * @param {String} sFilepath Path to the file to open
 * @returns Contents of the file
 * @type String
 * @exception ExFileLoadError
 */
EvalCodeHost.prototype.readFile = function (sFilepath) {
	var sourceFile = Cc["@mozilla.org/file/local;1"].
		createInstance(Ci.nsILocalFile);
	sourceFile.initWithPath(sFilepath);

	if (!sourceFile.exists() || !sourceFile.isFile()) {
		var sourceFilePath = sourceFile.path;
		sourceFile = null;
		throw(new ExFileLoadError("Unable to load file \"" + sourceFilePath + "\""));
	}

	var sourceFileStream = Cc["@mozilla.org/network/file-input-stream;1"].
		createInstance(Ci.nsIFileInputStream);
	sourceFileStream.init(sourceFile, 1, 0, false);
	var sourceFileBinStream = Cc["@mozilla.org/binaryinputstream;1"].
		createInstance(Ci.nsIBinaryInputStream);
	sourceFileBinStream.setInputStream(sourceFileStream);
	var sourceContents = sourceFileBinStream.readBytes(sourceFile.fileSize);
	
	sourceFile = null;
	sourceFileBinStream = null;
	sourceFileStream.close();
	sourceFileStream = null;

	return(sourceContents);
}


/**
 * Loads script file and evaluates it. Errors reading the
 *  source file results in an exception.
 * @param {String} sFilepath Path to the file to include
 * @param {Boolean} rawJs (optional) Flag to indicate that
 *  the file to be included is a plain JavaScript file with
 *  no containing script tags. This is typically used when a
 *  dynamic webpage needs to include a JavaScript library
 *  written for the client side
 * @exception ExScriptLoadError
 */
EvalCodeHost.prototype.require = function (sFilepath, rawJs) {
	rawJs = !!rawJs;

	sourceFile = this.script.mapPath(sFilepath);

	var sourceContents;
	try {
		sourceContents =
			(rawJs ? '<?js' : '') +
			this.readFile(sourceFile.path)
			+ (rawJs ? '?>' : '');
	} catch (ex) {
		throw((ex instanceof ExFileLoadError) ? new ExScriptLoadError("Unable to load include \"" + sFilepath + "\"") : ex);
	}

	this.eval(this.parse(sourceContents));
}


/**
 * Same as {@link #require} except that errors reading the
 *  source file do not result in an exception. Failure is
 *  instead communicated by the return value
 * @param {String} sFilepath Path to the file to include
 * @param {Boolean} rawJs (optional) Flag to indicate that
 *  the file to be included is a plain JavaScript 
 * @returns True/False indicating success in reading the file
 * @type Boolean
 * @see #require
 */
EvalCodeHost.prototype.include = function (sFilepath, rawJs) {
	try {
		this.require(sFilepath, rawJs);
	} catch (e) {
		if (!(e instanceof ExScriptLoadError)) { throw(e); }
		return(false);
	}
	return(true);
}


/**
 * Opens a URL and creates a stream to be read from
 * @param {String} sUrl URL to open
 * @param {Number} depth (optional) Number of redirects to allow before giving up
 * @returns An array containing the channel object and an open stream
 * @type [nsIHttpChannel, nsIBinaryInputStream]
 * @exception ExFileLoadError
 */
EvalCodeHost.prototype.openNetFile = function (sUrl, depth) {
	if (depth == null)
		depth = 5;

	var ioService = Cc["@mozilla.org/network/io-service;1"].
		getService(Ci.nsIIOService);
	var oUrl = ioService.newURI(sUrl, null, null);
	var channel = ioService.newChannelFromURI(oUrl);
	// TODO: check for other supported channel types
	if (!(channel instanceof Ci.nsIHttpChannel)) { throw(new ExFileLoadError("Unable to open file due to unsupported protocol/channel")); }
	var channelHttp = channel.QueryInterface(Ci.nsIHttpChannel);
	channelHttp.referrer = oUrl;
	var inStream = Cc["@mozilla.org/binaryinputstream;1"].
		createInstance(Ci.nsIBinaryInputStream);
	inStream.setInputStream(channelHttp.open());
	if (!channelHttp.requestSucceeded) {
		if (channelHttp.responseStatus == "302" || channelHttp.responseStatus == "301")
			if (depth > 1)
				return(this.openNetFile(channelHttp.getResponseHeader("location"), depth - 1));
			else
				throw(new ExFileLoadError("Unable to load file due to redirection limit"));
		else
			throw(new ExFileLoadError("Unable to load file due to failed request"));
	}
	return({
		"channel" : channelHttp,
		"stream" : inStream
		});
}


/**
 * Reads a file from the network
 * @param {String} sUrl URL to open
 * @returns Body of the response
 * @type String
 * @exception ExFileLoadError
 */
EvalCodeHost.prototype.readNetFile = function (sUrl) {
	var netFile;
	var sContent = "";

	try {
		// TODO: don't assume http channel
		netFile = this.openNetFile(sUrl);
		if (!netFile.channel.requestSucceeded) { throw(new ExFileLoadError("Unable to load file due to failed request")); }
		while (netFile.stream.available() || sContent.length < netFile.channel.contentLength) {
			sContent += netFile.stream.readBytes(netFile.stream.available());
		}
		netFile.stream.close();
		netFile.stream = null;
	} catch (ex) {
		if (netFile.stream != null) {
			netFile.stream.close();
			netFile.stream = null;
		}
		var newEx = new ExFileLoadError("Unable to load file");
		newEx.originalEx = ex;
		throw(newEx);
	}

	return(sContent);
}

/**
 * Creates missing directories in a path
 * @param {String} directory Path to build out
 */
EvalCodeHost.prototype.createPath = function (directory) {
	var buildPath = Cc["@mozilla.org/file/local;1"].
		createInstance(Ci.nsILocalFile);
	buildPath.initWithPath(directory);

	if (buildPath.exists())
		return;
	
	/*
	 * nsIFile.parent is broken. When the path is "/", parent should be
	 * null but it turns into "/.." instead and parent.parent is "/../.." etc...
	 */
	var depth = 0;
	var dirs = new Array(buildPath);
	var tmpDir = buildPath.parent;
	while (depth++ < 128 && tmpDir != null && tmpDir.path != this.server.tools.pathSeparator) {
		dirs.push(tmpDir);
		tmpDir = tmpDir.parent;
	}
	while (dirs.length) {
		var dir = dirs.pop();
		if (!dir.exists())
			dir.create(1 /* DIRECTORY_TYPE */, 0x0755);
	}
}

/**
 * Copies a file
 * @param {String} file Path to the source file
 * @param {String} directory Path to copy the file to
 * @param {Boolean} overwrite indicating whether existing files should be overwritten
 * @returns True/False indicating copy was a success
 * @type Boolean
 */
EvalCodeHost.prototype.copyFile = function (file, directory, overwrite) {
	overwrite = (overwrite == null) ? false : overwrite;

	var sourceFile = Cc["@mozilla.org/file/local;1"].
		createInstance(Ci.nsILocalFile);
	sourceFile.initWithPath(file);

	if (!sourceFile.exists())
		return(false);

	var destDirectory = Cc["@mozilla.org/file/local;1"].
		createInstance(Ci.nsILocalFile);
	destDirectory.initWithPath(directory);

	var destFile = destDirectory.clone();
	destFile.append(sourceFile.leafName);

	if (destFile.exists())
		if (!overwrite)
			return(false);
		else
			destFile.remove(true);

	this.createPath(directory);

	sourceFile.copyTo(destDirectory, "");

	return(true);
}


/********************************************************************************
 * Querystring
 *******************************************************************************/

/**
 * @class Class to handle encoding/decoding of URL formatted name/value pairs
 * @constructor
 * @param {String} encodedData (optional) URL encoded string
 */
function Querystring (encodedData) {
	this.parse((encodedData == null) ? "" : encodedData.toString());
}


/**
 * Serializer
 * @type String
 */
Querystring.prototype.toString = function () {
	var result = "";
	for (var key in this) {
		if (typeof this[key] != "function")
			result += (result.length ? "&" : "")
				+ escape(key)
				+ ((this[key] != "") ? ("=" + escape(this[key])) : "");
	}
	return(result);
}


/**
 * Parses a URL encoded string, breaking it up into a Hash
 * @param {String} data URL encoded string
 */
Querystring.prototype.parse = function (data) {
	if (data == null)
		return;
	var queryParts = data.split("&");
	for (var i = 0; i < queryParts.length; i++) {
		if (!queryParts[i].length) { continue; }
		var parameterParts = queryParts[i].split("=");
		if (parameterParts.length == 1) { parameterParts[1] = ""; }
		this[unescape(parameterParts.shift())] = unescape(parameterParts.join('='));
	}
}


/**
 * Provides a list of existing keys (names)
 * @returns Array of keys (names)
 * @type String[]
 */
Querystring.prototype.GetKeys = function () {
	var results = new Array();
	for (var key in this)
		if (typeof this[key] != "function")
			results[results.length] = key;
	return(results);
}


/********************************************************************************
 * Cookie
 *******************************************************************************/

/**
 * @class Class to represent an HTTP cookie
 * @constructor
 * @param {String} name Name of the cookie
 * @param {String} value Cookie contents
 * @requires Querystring
 */
function Cookie (name, value) {
	/**
	 * Name of the cookie
	 * @type String
	 */
	this.name = name;
	/** @private */
	this.watch("value", function (property, oldVal, newVal) { return(new Querystring(newVal)); });
	/**
	 * Contents of the cookie
	 * @type Querystring
	 */
	this.value = value;
	/**
	 * Path for the cookie
	 * @type String
	 */
	this.path = null;
	/**
	 * Cookie's domain
	 * @type String
	 */
	this.domain = null;
	/** @private */
	this.watch("expires", function (property, oldVal, newVal) {
		var date = null;
		if (!(newVal instanceof Date))
			try { date = new Date(newVal) } catch (e) { }
		return(date);
		});
	/**
	 * Expiration time of the cookie
	 * @type Date
	 */
	this.expires = null;
	/**
	 * Flag indicating whether the cookie is for HTTPS only
	 * @type Boolean
	 */
	this.secure = false;
}


/**
 * Runs cleanup on internal objects
 */
Cookie.prototype.destroy = function () {
	this.unwatch("value");
	this.unwatch("expires");
}


/**
 * Serializer
 * @type String
 */
Cookie.prototype.toString = function () {
	return(
		"Set-Cookie: "
		+ escape(this.name) + "=" + escape(this.value)
		+ (this.path ? "; path=" + escape(this.path) : "")
		+ (this.domain ? "; domain=" + escape(this.domain) : "")
		+ (this.expires ? "; expires=" + this.expires.toGMTString() : "")
		+ (this.secure ? "; secure" : "")
		);
}


/**
 * Parses a {@link Request#headers}, extracting any cookies found
 * @param {Request.headers} header {@link Request#headers Headers} collection of a {@link Request} object
 * @returns Collection of cookies found
 * @type {@link Cookie}[]
 */
Cookie.parse = function (header) {
	var results = new Array();
	if (!header)
		return(results);
	var cookies = header.toString().split(/(?:;\s*)/);
	for (var i = 0; i < cookies.length; i++) {
		var pieces = cookies[i].split(/=/);
		var key = unescape(pieces.shift());
		results[key] = new Cookie(key, unescape(pieces.join('=')));
	}
	return(results);
}


/********************************************************************************
 * Request
 *******************************************************************************/

/**
 * @class Class to represent an HTTP request
 * @constructor
 */
function Request (/* handlerObj */) {
	//this.handlerObj = handlerObj; // ???
	/**
	 * Headers collection
	 * @type Hash
	 */
	this.headers = new Array();
	this.headers['content-length'] = 0;
	this.headers['content-type'] = '';
	/**
	 * Cookies collection
	 * @type Cookie[]
	 */
	this.cookies = new Array();
	/**
	 * Querystring (GET) parameters
	 * @type Querystring
	 */
	this.querystring = new Array();
	this.watch("querystring", function (property, oldVal, newVal) { return(new Querystring(newVal)); });
	/**
	 * Form (POST) parameters
	 * @type Querystring
	 */
	this.form = new Array();
	this.watch("form", function (property, oldVal, newVal) { return(new Querystring(newVal)); });
	/**
	 * Request methods
	 * @type String
	 */
	this.method = "GET";
	/**
	 * Complete path/querystring of the resource requested
	 * @type String
	 */
	this.url = "/";
	/**
	 * HTTP version specified by the client
	 * @type String
	 */
	this.httpVersion = "HTTP/1.0";
	/**
	 * Path to the resource requested
	 * @type String
	 */
	this.path = "/";
	/**
	 * Complete, unparsed request
	 * @type String
	 */
	this.raw = "";
	/**
	 * Complete, unparsed headers
	 * @type String
	 */
	this.rawHeaders = "";
	/**
	 * Complete, unparsed querystring
	 * @type String
	 */
	this.rawQuerystring = "";
	/**
	 * Complete, unparsed body
	 * @type String
	 */
	this.rawBody = "";

	/** @private */
	this.headersComplete = false;
}


/**
 * Attempts to parse a raw request. Returns false until it is successful.
 * @param {String} sData Data from the incoming request. Data is dealt with
 *  in a chunked fashion (<i>Request</i> maintains all the data internally) so if
 *  calling <i>parse()</i> more than once, only new data should be passed each time. 
 * @returns True/False indicating whether the request has been completely parsed
 * @type Boolean
 */
Request.prototype.parse = function (sData) {
	if (sData != null && sData.length) {
		this.raw += sData;
	}
	if (!this.raw.match(/(\r\n\r\n|\r\r|\n\n)/)) {
		return(false);
	}

	if (!this.headersComplete) {
		[this.rawHeaders, this.rawBody] = this.raw.split(/(?:\r\n\r\n|\r\r|\n\n)/);

		var headers = this.rawHeaders.split(/(?:\r\n|\r|\n)/); // TODO: handle multiline headers
		if (!headers.length) { throw(new ExHttpParseError("Unable to find headers in request [[" + this.rawHeaders + "]]")); }

		[
			this.method,
			this.url,
			this.httpVersion
			] = headers[0].split(/\s/);
		if (this.method == null || this.url == null || this.httpVersion == null) {
 			throw(new ExHttpParseError("Unable to find request method and url [["+ headers[0] +"]]"));
		}
		this.method = this.method.toLowerCase();

		[this.path, this.rawQuerystring] = this.url.split(/\?/);
		this.querystring = this.rawQuerystring;

		for (var i = 1; i < headers.length; i++) {
			var pieces = headers[i].split(/\:\s*/);
			if (pieces.length < 2) { JsWs.prototype.log("Error on header split: " + headers[i]); continue; }
			this.headers[pieces.shift().toLowerCase()] = pieces.join(':');
		}

		this.cookies = Cookie.parse(this.headers["cookie"]);

		this.headersComplete = true;
	}
	else {
		this.rawBody += sData;
	}

	if (
		(this.method == "post")
		&&
		(this.headers['content-length'] == this.rawBody.length)
		&&
		(this.headers['content-type'].indexOf("application/x-www-form-urlencoded") > -1)
		){
		this.form = this.rawBody;
	}

	return(
		this.headersComplete &&
		(
			this.headers['content-length'] == null
			||
			this.headers['content-length'] == this.rawBody.length
			)
		);
}


/**
 * Runs cleanup on internal objects
 */
Request.prototype.destroy = function () {
	//this.handlerObj = null; // ???
	this.headers = null;
	for (var i = 0; i < this.cookies.length; i++) {
		this.cookies[i].destroy();
		this.cookies[i] = null;
	}
	this.cookies = null;
	this.unwatch("querystring");
	this.querystring = null;
	this.unwatch("form");
	this.form = null;
	this.path = null;
	this.raw = null;
	this.rawHeaders = null;
	this.rawQuerystring = null;
	this.rawBody = null;
}


/********************************************************************************
 * Response
 *******************************************************************************/
/**
 * @class Class to represent an HTTP response
 * @constructor
 */
function Response (handlerObj) {
	/** @private */
	this.handlerObj = handlerObj;
	/**
	 * Flag to indicate whether output should be buffered or sent immediately
	 * @type Boolean
	 */
	this.buffer = true;
	/** @private */
	this.headersSent = false;
	/** @private */
	this.output = new Array();
	/**
	 * HTTP status code
	 * @type Number
	 */
	this.status = 200;
	/**
	 * Headers collection
	 * @type Hash
	 */
	this.headers = new Array();
	/**
	 * Cookies collection
	 * @type Cookie[]
	 */
	this.cookies = new Array();

	this.contentType("text/html");
}


/**
 * Write data to output (client)
 * @param {String} sData Data to write
 */
Response.prototype.write = function (sData) {
	if (this.buffer) {
		this.output[this.output.length] = sData;
	} else {
		this.handlerObj.write(this.renderHeaders());
		this.handlerObj.write(sData);
	}
}


/**
 * Writes any buffered data to output (client)
 */
Response.prototype.flush = function () {
	this.handlerObj.write(this.renderHeaders());
	if (this.output.length)
		this.handlerObj.write(this.output.join('')); this.output = new Array();
	//this.buffer = false; // ???
}


/**
 * Removes any buffered output
 */
Response.prototype.clear = function () {
	this.output = new Array();
}


/**
 * Immediately halts execution of the current page. Any buffered content
 * 	is flushed.
 * @exception ExResponseEnd
 */
Response.prototype.end = function () {
	if (this.buffer) {
		this.headers["Content-Length"] = this.output.join('').length;
	}
	this.flush();
	throw(new ExResponseEnd());
}


/**
 * Sets the content-type header of the response
 * @param {String} sType MIME type
 */
Response.prototype.contentType = function (sType) {
	this.headers["Content-Type"] = sType;
}


/**
 * Causes the response to issue a redirect to the client.
 * @param {String} sLocation URL to redirect to. Passing a null
 *  clears the redirect
 */
Response.prototype.redirect = function (sLocation) {
	if (sLocation != null) {
		this.headers["Location"] = sLocation;
		this.status = 302;
	} else {
		delete this.headers["Location"]; // ???
		this.status = 200; // ???
	}
}


/**
 * Adds a cookie to the response
 * @param {Cookie} cookie Cookie to add
 */
Response.prototype.setCookie = function (cookie) {
	this.cookies[cookie.name] = cookie;
}


/** @private */
Response.prototype.statText = {
	200: "OK",
	301: "Moved Permanently",
	302: "Found",
	304: "Not Modified",
	400: "Bad Request",
	401: "Unauthorized",
	403: "Forbidden",
	404: "Not Found",
	405: "Method Not Allowed",
	500: "Internal Server Error",
	501: "Not Implemented",
	503: "Service Unavailable",
	505: "HTTP Version Not Supported"
};

/** @private */
Response.prototype.renderStatus = function () {
	return("HTTP/1.1 " + this.status + " " + ((this.statText[this.status] == null) ? "UNKNOWN" : this.statText[this.status]) + "\r\n");
}


/** @private */
Response.prototype.renderHeaders = function () {
	if (this.headersSent) { return(""); }
	this.headersSent = true;
	var sRetval = this.renderStatus();
	for (var key in this.headers)
		sRetval += key + ": " + this.headers[key] + "\r\n";
	for (var key in this.cookies)
		sRetval += this.cookies[key] + "\r\n";
	return(sRetval + "\r\n");
}


/**
 * Runs cleanup on internal objects
 */
Response.prototype.destroy = function () {
	this.handlerObj.destroy();
	this.handlerObj = null;
	this.output = null;
	this.headers = null;
	for (var i = 0; i < this.cookies.length; i++) {
		this.cookies[i].destroy();
		this.cookies[i] = null;
	}
	this.cookies = null;
	this.contentType = null;
}


/********************************************************************************
 * ScriptContext
 *******************************************************************************/

/**
 * @class Provides utilities and context to dynamic webpages
 * @constructor
 * @param {JsWsConnectionHandler} serverObj JsWs server object
 * @param {String} sPath Location of the currently executing script
 */
function ScriptContext (serverObj, sPath) {
	/** @private */
	this.serverObj = serverObj;
	/**
	 * Path to the requested webpage
	 * @type nsILocalFile
	 */
	this.path = Cc["@mozilla.org/file/local;1"].
		createInstance(Ci.nsILocalFile);
	if (sPath.indexOf(this.serverObj.tools.pathSeparator) != 0) {
		sPath = this.serverObj.tools.pathSeparator + sPath;
	}
	this.path.initWithPath(sPath); // !!!

	/**
	 * Path to the folder containing the requested webpage
	 * @type nsILocalFile
	 */
	this.root = "";
	var fullScPath = this.mapPath(this.path.path);
	if (fullScPath.exists()) {
		if (fullScPath.isDirectory()) {
			var tmpPath = this.path.clone();
			this.serverObj.tools.appendPath(tmpPath, "index." + JSWS_SCRIPT_FILEEXT);
			if (tmpPath.exists() && tmpPath.isFile()) {
				this.path = tmpPath;
			}
			this.root = this.path.clone();
		}
		else
			this.root = this.path.parent;
	}
	else {
		this.root = this.path.parent;
	}
}


/**
 * Runs cleanup on internal objects
 */
ScriptContext.prototype.destroy = function () {
	this.serverObj = null;
	this.path = null;
	this.root = null;
}

/**
 * Calculates an absolute path. The passed-in path is treated as relative to server's
 *  document root if the path begins with a "/". It is treated as relative to the
 *  executing script if the path does not begin with a "/".
 * @param {String} path Path to resolve to an absolute path
 * @returns Full path to in the context of the local filesystem
 * @type nsILocalFile
 */
ScriptContext.prototype.mapPath = function (path) {
	path = path.replace(/(\/|\\)/g, this.serverObj.tools.pathSeparator);
	if (path.indexOf(this.serverObj.tools.pathSeparator) != 0) {
		var oRoot = this.root.clone();
		this.serverObj.tools.appendPath(oRoot, path);
		path = oRoot.path;
	}
	var mappedModule = this.serverObj.mapModule(path);
	var virtualPathLength = mappedModule[JSWS_MODULES_VIRTUALPATHKEY].length;
	var finalPath = mappedModule[JSWS_MODULES_EXTENSIONPATHKEY]
		+ mappedModule[JSWS_MODULES_CONTENTPATHKEY];
	var oFile = Cc["@mozilla.org/file/local;1"].
		createInstance(Ci.nsILocalFile);
	oFile.initWithPath(finalPath);
	this.serverObj.tools.appendPath(oFile, path.substring(virtualPathLength, path.length));
	return(oFile);
}


/********************************************************************************
 * JsWsConnectionHandler
 *******************************************************************************/

/**
 * @class Handles an incoming socket connection
 * @constructor
 * @param {JsWsListener} server JsWs server socket listener
 * @param {nsIServerSocket} socket Incoming accepted socket
 * @param {nsISocketTransport} transport Socket transport
 */
function JsWsConnectionHandler (server, socket, transport) {
	/**	
	 * JsWs server socket listener
	 * @type JsWsListener
	 */
	this.server = server;
	/**	
	 * Incoming accepted socket
	 * @type nsIServerSocket
	 */
	this.socket = socket;
	/**	
	 * Socket transport
	 * @type nsISocketTransport
	 */
	this.transport = transport;
	/**	
	 * Request object
	 * @type Request
	 */
	this.request = new Request();
	/**	
	 * Response object
	 * @type Response
	 */
	this.response = new Response(this);
	
	if (transport) {
		/** @private */
		this.outstream = this.transport.openOutputStream(1, 0, 0);
		/** @private */
		this.stream = this.transport.openInputStream(0, 0, 0);
		/** @private */
		this.instream = Cc["@mozilla.org/binaryinputstream;1"].
			createInstance(Ci.nsIBinaryInputStream);
		this.instream.setInputStream(this.stream);
		/** @private */
		this.dataPump = Components.
			classes["@mozilla.org/network/input-stream-pump;1"].
			createInstance(Ci.nsIInputStreamPump);
		this.dataPump.init(this.stream, -1, -1, 0, 0, false);
		this.dataPump.asyncRead(this, null);
	}
}


/**
 * Runs cleanup on internal objects
 */
JsWsConnectionHandler.prototype.destroy = function () {
	this.server = null;
	this.socket = null;
	this.transport = null;
	if (this.outstream)
		this.outstream.close();
	this.outstream = null;
	if (this.request)
		this.request.destroy();
	this.request = null;
	if (this.response)
		this.response.destroy();
	this.response = null;
	if (this.instream)
		this.instream.close();
	this.instream = null;
	if (this.stream)
		this.stream.close();
	this.stream = null;
	this.dataPump = null;
}


/** @private */
JsWsConnectionHandler.prototype.onStartRequest = function (request, context) { }

/** @private */
JsWsConnectionHandler.prototype.onStopRequest = function (request, context, status) {
	this.destroy();
}


/**
 * Writes the data to the connected socket's output stream
 * @param {String} sData Data to write to the stream
 */
JsWsConnectionHandler.prototype.write = function (sData) {
	var realSize = sData.length;
	var writeSize = this.outstream.write(sData, sData.length);
}


/** @private */
JsWsConnectionHandler.prototype.handleRequest = function () {
	var script = new ScriptContext(this.server, this.request.path);
	var codeHost = new EvalCodeHost(this.server, this.request, this.response, script);

	try {
		try {
			var mappedModule = this.server.mapModule(script.path.path);
			if (!mappedModule[JSWS_MODULES_HANDLERKEY])
				mappedModule = this.server.modules[JSWS_MODULES_ROOTKEY];
			//codeHost.require("/../pageHandler." + JSWS_SCRIPT_FILEEXT);
			codeHost.require(
				mappedModule[JSWS_MODULES_VIRTUALPATHKEY] +
				mappedModule[JSWS_MODULES_HANDLERKEY]
				);
		} catch (ex) {
			if (!(ex instanceof ExResponseEnd)) {
				var _ex = (ex instanceof ExBase) ? ex : new ExBase("Uncaught top-level", 0, ex);
				this.response.status = 500;
				this.response.clear();
				this.response.contentType("text/html");
				this.response.write("<hr><pre>Server Error: " + _ex + "</pre><hr>");
			}
		}
		this.response.end();
	} catch (e) { }

	codeHost.destroy();
	codeHost = null;
	script.destroy();
	script = null;
}


/** @private */
JsWsConnectionHandler.prototype.onDataAvailable = function (request, context, inputStream, offset, count) {
	try {
		// TODO: Handle chunked input from body (uploads)
		if (this.request.parse(this.instream.readBytes(this.instream.available()))) {
			this.handleRequest();
			this.instream.close();
		}
	} catch (e) { JsWs.prototype.log(e); }
}


/********************************************************************************
 * JsWsListener
 *******************************************************************************/

/**
 * @class Socket listener and host to all server scope varaibles and objects
 * @constructor
 */
function JsWsListener () {
	/**
	 * Hash to store server scoped data for sharing between requests and pages
	 * @type Hash
	 */
	this.state = new Array();
	/** @private */
	this.modules = new Array();
	/**
	 * Path to the server's root
	 * @type nsILocalFile
	 */
	this.root = Cc["@mozilla.org/file/directory_service;1"].
		getService(Ci.nsIProperties).
		get('ProfD', Ci.nsILocalFile);
	/**
	 * Path to the server's document root
	 * @type nsILocalFile
	 */
	this.documentRoot = this.root.clone();
	this.tools.appendPath(this.documentRoot, JSWS_DOCROOT);

	this.loadModules();

	for (var key in this.modules) {
		if (this.modules[key][JSWS_MODULES_INITPAGEKEY]) {
			var mappedModule = this.modules[key];
			var handlerObj = {
					"write" : function () {},
					"destroy" : function () {}
					}
			var request = new Request();
			var response = new Response(handlerObj);
			var script = new ScriptContext(this, request.path);
			var codeHost = new EvalCodeHost(this, request, response, script);

			try {
				codeHost.require(
					mappedModule[JSWS_MODULES_VIRTUALPATHKEY] +
					mappedModule[JSWS_MODULES_INITPAGEKEY]
					);
				JsWs.prototype.log("Module \"" + mappedModule[JSWS_MODULES_NAMEKEY] + "\" initialized");
			} catch (ex) {
				JsWs.prototype.log("Init page for module \"" + mappedModule[JSWS_MODULES_NAMEKEY] + "\" failed : " + ex);
			} finally {
				request.destroy();
				request = null;
				response.destroy();
				response = null;
				script.destroy();
				script = null;
				codeHost.destroy();
				codeHost = null;
			}
		}
	}
}


/**
 * Runs cleanup on internal objects
 */
JsWsListener.prototype.destroy = function () {
	this.state = null;
	this.modules = null;
	this.root = null;
	this.documentRoot = null;
}


/** @private */
JsWsListener.prototype.loadModules = function () {
	try {
		var extManager = Cc["@mozilla.org/extensions/manager;1"]
			.createInstance(Ci.nsIExtensionManager);
		var prefSvc = Cc["@mozilla.org/preferences-service;1"]
			.getService(Ci.nsIPrefService);
		var prefBranch = prefSvc.getBranch(JSWS_PREF_MODULES_ROOT);
		var children = prefBranch.getChildList("", {});
		for (var i = 0; i < children.length; i++) {
			var child = children[i];
			var parts = child.split('.');
			var module = parts.shift();
			var key = parts.join('.');
			if (!this.modules[module])
				this.modules[module] = new Array();
			this.modules[module][key] = prefBranch.getCharPref(child);
			if (key == JSWS_MODULES_IDKEY) {
				this.modules[module][JSWS_MODULES_EXTENSIONPATHKEY] =
					extManager
					.getInstallLocation(this.modules[module][key])
					.getItemLocation(this.modules[module][key])
					.path;
			}
		}
	} catch (e) { }

	this.modules[JSWS_MODULES_ROOTKEY] = new Array();
	this.modules[JSWS_MODULES_ROOTKEY][JSWS_MODULES_IDKEY] = JSWS_MODULES_ROOTKEY;
	this.modules[JSWS_MODULES_ROOTKEY][JSWS_MODULES_NAMEKEY] = JSWS_MODULES_ROOTKEY;
	this.modules[JSWS_MODULES_ROOTKEY][JSWS_MODULES_EXTENSIONPATHKEY] = this.root.path;
	this.modules[JSWS_MODULES_ROOTKEY][JSWS_MODULES_CONTENTPATHKEY] = JSWS_DOCROOT;
	this.modules[JSWS_MODULES_ROOTKEY][JSWS_MODULES_VIRTUALPATHKEY] = "/";
	this.modules[JSWS_MODULES_ROOTKEY][JSWS_MODULES_HANDLERKEY] = "../pageHandler." +  JSWS_SCRIPT_FILEEXT;
	this.modules[JSWS_MODULES_ROOTKEY][JSWS_MODULES_STARTPAGEKEY] = "/index." +  JSWS_SCRIPT_FILEEXT;
}

/**
 * Maps a path to the appropriate module
 * @param {String} path Absolute server path to find the mapped module for
 * @returns Module that handles the path
 * @type Module
 */
JsWsListener.prototype.mapModule = function (path) {
	path = this.tools.normalizePath(path);

	var mappedModule = this.modules[JSWS_MODULES_ROOTKEY];
	for (var key in this.modules) {
		if (key == JSWS_MODULES_ROOTKEY)
			continue;
		if (path.indexOf(this.modules[key][JSWS_MODULES_VIRTUALPATHKEY]) == 0)
			if (this.modules[key][JSWS_MODULES_VIRTUALPATHKEY].length > mappedModule[JSWS_MODULES_VIRTUALPATHKEY].length)
				mappedModule = this.modules[key];
	}

	return(mappedModule);
}

/** @private */
JsWsListener.prototype.onSocketAccepted = function (socket, transport) {
	new JsWsConnectionHandler(this, socket, transport);
}


/** @private */
JsWsListener.prototype.onStopListening = function (socket, status) {}

/** TODO */
JsWsListener.prototype.tools = {

	"normalizePath": function (path) {
			return(path.replace(/(\/|\\)/g, this.pathSeparator));
		},

	"htmlEscape": function (sContent) {
			return(sContent.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/\n/g, "<br>\n"));
		},

	"pathSeparator": Cc["@mozilla.org/file/directory_service;1"].
		getService(Ci.nsIProperties).
		get('ProfD', Ci.nsILocalFile).
		path.match('/') ? '/' : '\\',

	"appendPath": function (oFile, sPath) {
		var oLocalFile = oFile.QueryInterface(Ci.nsILocalFile);
		var pathParts = sPath.split(/(?:\/|\\)+/);
		for (var i = 0; i < pathParts.length; i++) {
			if (pathParts[i] == '.') { continue; }
			if (pathParts[i] == '..') {
				oLocalFile.initWithPath(oLocalFile.parent.path);
			} else {
				oLocalFile.append(pathParts[i]);
			}
		}
		oLocalFile.normalize();
	},

	"enumTypes": function (sType, oTarget) {
		var objs = new Array();
		if (oTarget == null) { oTarget = window; }

		for (var i in oTarget) {
			try {
				if (sType == null | typeof oTarget[i] == sType) {
					objs[i] = typeof oTarget[i];
				}
			} catch (e) {
				// Hmmm
			}
		}
		return(objs);
	},

	"enumFunctions": function (oTarget) {return(enumTypes("function", oTarget)); },

	"enumStrings": function (oTarget) { return(enumTypes("string", oTarget)); },

	"enumObjects": function (oTarget) { return(enumTypes("object", oTarget)); },

	"enumNumbers": function (oTarget) { return(enumTypes("number", oTarget)); },

	"enumBools": function (oTarget) { return(enumTypes("boolean", oTarget)); }

}

/********************************************************************************
 * JsWs
 *******************************************************************************/
/**
 * @class JsWs server object. Exposes high-level starting and stopping of the server
 * @contructor
 * @param {Number} port Port to listen on
 * @param {Boolean} localOnly Flag indicating whether to ignore requests from remote addresses
 * @param {Boolean} threaded Flag indicating whether the server should run in thread separate
 *  from the main JS/GUI thread
 */ 
function JsWs (port, localOnly, threaded) {
	/** @private */
	this.port = port;
	/** @private */
	this.localOnly = !!localOnly;
	/** @private */
	this.isThreaded = !!threaded;
}


/** @private */
JsWs.prototype.QueryInterface = function (aIID) {
	if(
		!aIID.equals(Ci.nsISupports) &&
		!aIID.equals(Ci.nsIRunnable) &&
		!aIID.equals(Ci.nsIClassInfo)
		)
		throw Components.results.NS_ERROR_NO_INTERFACE;

	return this;
}


/** Starts the server listening */
JsWs.prototype.start = function () {
	/* Something wierd is going on with the math functions.
	 * Looks like FF has major problems if the math library is
	 * called/loaded for the first time by something other
	 * than the main JS thread. Hence the stupidness that follows...
	 * KLUDGE ZONE */
	parseInt("1");
	new Number(1);
	/* KLUDGE ZONE */

	var currentThread = Cc["@mozilla.org/thread;1"]
		.createInstance(Ci.nsIThread)
		.currentThread;

	this.log("Server Starting [port:" + this.port + "]");
	this.eqs = Cc["@mozilla.org/event-queue-service;1"]
		.getService(Ci.nsIEventQueueService);
	this.server = Cc["@mozilla.org/network/server-socket;1"]
		.createInstance(Ci.nsIServerSocket);
	this.server.init(this.port, this.localOnly, -1);
	this.listener = new JsWsListener();
	currentThread.sleep(200);
	if (this.isThreaded) {
		this.thread = Cc["@mozilla.org/thread;1"]
			.createInstance(Ci.nsIThread);
		this.thread.init(
			this,
			0,
			Ci.nsIThread.PRIORITY_LOW,
			Ci.nsIThread.SCOPE_LOCAL,
			Ci.nsIThread.STATE_UNJOINABLE
			);
	}
	else {
		this.server.asyncListen(this.listener);
	}
	this.log("Server Started");
}


/**
 * Runs cleanup on internal objects
 */
JsWs.prototype.destroy = function () {
	this.stop();
	this.eqs = null;
	this.server = null;
	if (this.listener)
		this.listener.destroy();
	this.listener = null;
}


/** Stops the server listening */
JsWs.prototype.stop = function () {
	this.log("Stopping...");
	if (this.isThreaded && this.thread)
		this.thread.interrupt();
	if (this.server)
		this.server.close();
	this.log("done");
}


/** @private */
JsWs.prototype.run = function () {
	this.eq = this.eqs.createFromIThread(this.thread, false);
	this.thread.sleep(300);
	this.server.asyncListen(this.listener);
	this.thread.sleep(300);
	this.eq.eventLoop();
	this.eq.processPendingEvents();
	this.eq.stopAcceptingEvents();
	this.eq.processPendingEvents();
	this.eqs.destroyThreadEventQueue();
}


/**
 * Logs a message to the JavaScript console
 * @param {String} s Message to log
 */
JsWs.prototype.log = function (s) {
	var currentThread = Cc["@mozilla.org/thread;1"]
		.createInstance(Ci.nsIThread)
		.currentThread;
	Cc["@mozilla.org/consoleservice;1"].
		getService(Ci.nsIConsoleService).
		logStringMessage("(" + currentThread.priority + ")[JsWs] " + s);
}


/** @private */
JsWs.prototype.getInterfaces = function (count) {
	var ifaces = [
		Ci.nsISupports,
		Ci.nsIRunnable,
		Ci.nsIClassInfo
		];
	count.value = ifaces.length;
	return ifaces;
}


/** @private */
JsWs.prototype.getHelperForLanguage = function (language) {
	return null;
}

// TODO: smart handling of string vs. nsIFile paths
